﻿// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CSharp.SyntaxWalkers;

namespace Roslynator.CSharp.Analysis.UnusedMember;

internal class UnusedMemberWalker : TypeSyntaxWalker
{
    [ThreadStatic]
    private static UnusedMemberWalker _cachedInstance;

    private bool _isEmpty;

    private IMethodSymbol _containingMethodSymbol;

    public Collection<NodeSymbolInfo> Nodes { get; } = [];

    public SemanticModel SemanticModel { get; set; }

    public CancellationToken CancellationToken { get; set; }

    public bool IsAnyNodeConst { get; private set; }

    public bool IsAnyNodeDelegate { get; private set; }

    protected override bool ShouldVisit
    {
        get { return !_isEmpty; }
    }

    public void Reset()
    {
        _isEmpty = false;
        _containingMethodSymbol = null;

        Nodes.Clear();
        SemanticModel = null;
        CancellationToken = default;
        IsAnyNodeConst = false;
        IsAnyNodeDelegate = false;
    }

    public void AddDelegate(string name, SyntaxNode node)
    {
        AddNode(name, node);

        IsAnyNodeDelegate = true;
    }

    public void AddNode(string name, SyntaxNode node)
    {
        Nodes.Add(new NodeSymbolInfo(name, node));
    }

    public void AddNodes(VariableDeclarationSyntax declaration, bool isConst = false)
    {
        foreach (VariableDeclaratorSyntax declarator in declaration.Variables)
            AddNode(declarator.Identifier.ValueText, declarator);

        if (isConst)
            IsAnyNodeConst = true;
    }

    private void RemoveNodeAt(int index)
    {
        Nodes.RemoveAt(index);

        if (Nodes.Count == 0)
            _isEmpty = true;
    }

    private void VisitSimpleName(SimpleNameSyntax node, string name)
    {
        for (int i = Nodes.Count - 1; i >= 0; i--)
        {
            NodeSymbolInfo info = Nodes[i];

            if (info.Name == name)
            {
                if (info.Symbol is null)
                {
                    ISymbol declaredSymbol = SemanticModel.GetDeclaredSymbol(info.Node, CancellationToken);

                    Debug.Assert(declaredSymbol is not null, "");

                    if (declaredSymbol is null)
                    {
                        RemoveNodeAt(i);
                        continue;
                    }

                    info = new NodeSymbolInfo(info.Name, info.Node, declaredSymbol);

                    Nodes[i] = info;
                }

                SymbolInfo symbolInfo = SemanticModel.GetSymbolInfo(node, CancellationToken);

                if (symbolInfo.Symbol is not null)
                {
                    ISymbol symbol = symbolInfo.Symbol;

                    if (symbol.Kind == SymbolKind.Method)
                    {
                        var methodSymbol = ((IMethodSymbol)symbol);

                        if (methodSymbol.MethodKind == MethodKind.ReducedExtension)
                            symbol = methodSymbol.ReducedFrom;
                    }

                    symbol = symbol.OriginalDefinition;

                    if (SymbolEqualityComparer.Default.Equals(info.Symbol, symbol)
                        && !SymbolEqualityComparer.Default.Equals(_containingMethodSymbol, symbol))
                    {
                        RemoveNodeAt(i);
                    }
                }
                else if (symbolInfo.CandidateReason == CandidateReason.LateBound)
                {
                    RemoveNodeAt(i);
                }
                else if (symbolInfo.CandidateReason == CandidateReason.MemberGroup
                    || symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure)
                {
                    ImmutableArray<ISymbol> candidateSymbols = symbolInfo.CandidateSymbols;

                    for (int j = 0; j < candidateSymbols.Length; j++)
                    {
                        ISymbol symbol = candidateSymbols[j].OriginalDefinition;

                        if (SymbolEqualityComparer.Default.Equals(info.Symbol, symbol)
                            && !SymbolEqualityComparer.Default.Equals(_containingMethodSymbol, symbol))
                        {
                            RemoveNodeAt(i);
                        }
                    }
                }
            }
        }
    }

    public override void VisitGenericName(GenericNameSyntax node)
    {
        VisitSimpleName(node, node.Identifier.ValueText);

        if (IsAnyNodeDelegate)
            VisitTypeArgumentList(node.TypeArgumentList);
    }

    public override void VisitIdentifierName(IdentifierNameSyntax node)
    {
        VisitSimpleName(node, node.Identifier.ValueText);
    }

    public override void VisitTypeArgumentList(TypeArgumentListSyntax node)
    {
        foreach (TypeSyntax type in node.Arguments)
        {
            if (!ShouldVisit)
                return;

            VisitType(type);
        }
    }

    protected override void VisitType(TypeSyntax node)
    {
        if (node is not null
            && IsAnyNodeDelegate)
        {
            Visit(node);
        }
    }

    public override void VisitGotoStatement(GotoStatementSyntax node)
    {
    }

    public override void VisitLiteralExpression(LiteralExpressionSyntax node)
    {
    }

    public override void VisitNameColon(NameColonSyntax node)
    {
    }

    public override void VisitExplicitInterfaceSpecifier(ExplicitInterfaceSpecifierSyntax node)
    {
        Debug.Fail($"{nameof(UnusedMemberWalker)}.{nameof(VisitExplicitInterfaceSpecifier)}");
    }

    public override void VisitTypeParameterList(TypeParameterListSyntax node)
    {
        Debug.Fail($"{nameof(UnusedMemberWalker)}.{nameof(VisitTypeParameterList)}");
    }

    public override void VisitBaseList(BaseListSyntax node)
    {
        if (node is not null)
            base.VisitBaseList(node);
    }

    public override void VisitTypeParameterConstraintClause(TypeParameterConstraintClauseSyntax node)
    {
        Debug.Fail($"{nameof(UnusedMemberWalker)}.{nameof(VisitTypeParameterConstraintClause)}");
    }

    public override void VisitParameterList(ParameterListSyntax node)
    {
        if (node is not null)
            base.VisitParameterList(node);
    }

    public override void VisitCompilationUnit(CompilationUnitSyntax node)
    {
        VisitMembers(node.Members);
    }

    public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node)
    {
        VisitMembers(node.Members);
    }

    public override void VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);
#if ROSLYN_4_7
        VisitParameterList(node.ParameterList);
#endif
        VisitBaseList(node.BaseList);
        VisitMembers(node.Members);
    }

    public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);
#if ROSLYN_4_7
        VisitParameterList(node.ParameterList);
#endif
        VisitBaseList(node.BaseList);
        VisitMembers(node.Members);
    }

    public override void VisitStructDeclaration(StructDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);
#if ROSLYN_4_7
        VisitParameterList(node.ParameterList);
#endif
        VisitBaseList(node.BaseList);
        VisitMembers(node.Members);
    }

    public override void VisitRecordDeclaration(RecordDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);
        VisitBaseList(node.BaseList);
        VisitMembers(node.Members);
    }

    public override void VisitDelegateDeclaration(DelegateDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);

        if (!ShouldVisit)
            return;

        TypeSyntax returnType = node.ReturnType;

        if (returnType is not null)
            VisitType(returnType);

        if (!ShouldVisit)
            return;

        VisitParameterList(node.ParameterList);
    }

    public override void VisitEventDeclaration(EventDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);

        if (!ShouldVisit)
            return;

        TypeSyntax type = node.Type;

        if (type is not null)
            VisitType(type);

        if (!ShouldVisit)
            return;

        AccessorListSyntax accessorList = node.AccessorList;

        if (accessorList is not null)
            VisitAccessorList(accessorList);
    }

    public override void VisitEventFieldDeclaration(EventFieldDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);
    }

    public override void VisitEnumDeclaration(EnumDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);

        if (IsAnyNodeConst)
        {
            foreach (EnumMemberDeclarationSyntax member in node.Members)
            {
                if (!ShouldVisit)
                    return;

                VisitEnumMemberDeclaration(member);
            }
        }
    }

    public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);

        if (!ShouldVisit)
            return;

        TypeSyntax type = node.Type;

        if (type is not null)
            VisitType(type);

        if (!ShouldVisit)
            return;

        EqualsValueClauseSyntax initializer = node.Initializer;

        if (initializer is not null)
            VisitEqualsValueClause(initializer);

        if (!ShouldVisit)
            return;

        AccessorListSyntax accessorList = node.AccessorList;

        if (accessorList is not null)
        {
            VisitAccessorList(accessorList);
        }
        else
        {
            ArrowExpressionClauseSyntax expressionBody = node.ExpressionBody;

            if (expressionBody is not null)
                VisitArrowExpressionClause(expressionBody);
        }
    }

    public override void VisitIndexerDeclaration(IndexerDeclarationSyntax node)
    {
        VisitAttributeLists(node.AttributeLists);

        if (!ShouldVisit)
            return;

        TypeSyntax type = node.Type;

        if (type is not null)
            VisitType(type);

        if (!ShouldVisit)
            return;

        BracketedParameterListSyntax parameterList = node.ParameterList;

        if (node is not null)
            VisitBracketedParameterList(parameterList);

        if (!ShouldVisit)
            return;

        AccessorListSyntax accessorList = node.AccessorList;

        if (accessorList is not null)
        {
            VisitAccessorList(accessorList);
        }
        else
        {
            ArrowExpressionClauseSyntax expressionBody = node.ExpressionBody;

            if (expressionBody is not null)
                VisitArrowExpressionClause(expressionBody);
        }
    }

    public override void VisitLocalFunctionStatement(LocalFunctionStatementSyntax node)
    {
        TypeSyntax returnType = node.ReturnType;

        if (returnType is not null)
            VisitType(returnType);

        if (!ShouldVisit)
            return;

        VisitParameterList(node.ParameterList);

        if (!ShouldVisit)
            return;

        BlockSyntax body = node.Body;

        if (body is not null)
        {
            VisitBlock(body);
        }
        else
        {
            ArrowExpressionClauseSyntax expressionBody = node.ExpressionBody;

            if (expressionBody is not null)
            {
                VisitArrowExpressionClause(expressionBody);
            }
        }
    }

    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        Debug.Assert(_containingMethodSymbol is null);

        _containingMethodSymbol = SemanticModel.GetDeclaredSymbol(node, CancellationToken);

        VisitAttributeLists(node.AttributeLists);

        if (!ShouldVisit)
            return;

        TypeSyntax returnType = node.ReturnType;

        if (returnType is not null)
            VisitType(returnType);

        if (!ShouldVisit)
            return;

        VisitParameterList(node.ParameterList);

        if (!ShouldVisit)
            return;

        BlockSyntax body = node.Body;

        if (body is not null)
        {
            VisitBlock(body);
        }
        else
        {
            ArrowExpressionClauseSyntax expressionBody = node.ExpressionBody;

            if (expressionBody is not null)
            {
                VisitArrowExpressionClause(expressionBody);
            }
        }

        _containingMethodSymbol = null;
    }

    public override void VisitStackAllocArrayCreationExpression(StackAllocArrayCreationExpressionSyntax node)
    {
        TypeSyntax type = node.Type;

        if (type is not null)
        {
            if (type.IsKind(SyntaxKind.ArrayType))
            {
                VisitArrayType((ArrayTypeSyntax)type);
            }
            else
            {
                VisitType(type);
            }
        }

        if (!ShouldVisit)
            return;

        InitializerExpressionSyntax initializer = node.Initializer;

        if (initializer is not null)
            VisitInitializerExpression(initializer);
    }

    private void VisitMembers(SyntaxList<MemberDeclarationSyntax> members)
    {
        foreach (MemberDeclarationSyntax memberDeclaration in members)
        {
            if (!ShouldVisit)
                return;

            VisitMemberDeclaration(memberDeclaration);
        }
    }

    private void VisitMemberDeclaration(MemberDeclarationSyntax node)
    {
        switch (node.Kind())
        {
            case SyntaxKind.ClassDeclaration:
                VisitClassDeclaration((ClassDeclarationSyntax)node);
                break;
            case SyntaxKind.ConstructorDeclaration:
                VisitConstructorDeclaration((ConstructorDeclarationSyntax)node);
                break;
            case SyntaxKind.ConversionOperatorDeclaration:
                VisitConversionOperatorDeclaration((ConversionOperatorDeclarationSyntax)node);
                break;
            case SyntaxKind.DelegateDeclaration:
                VisitDelegateDeclaration((DelegateDeclarationSyntax)node);
                break;
            case SyntaxKind.DestructorDeclaration:
                VisitDestructorDeclaration((DestructorDeclarationSyntax)node);
                break;
            case SyntaxKind.EnumDeclaration:
                VisitEnumDeclaration((EnumDeclarationSyntax)node);
                break;
            case SyntaxKind.EnumMemberDeclaration:
                VisitEnumMemberDeclaration((EnumMemberDeclarationSyntax)node);
                break;
            case SyntaxKind.EventDeclaration:
                VisitEventDeclaration((EventDeclarationSyntax)node);
                break;
            case SyntaxKind.EventFieldDeclaration:
                VisitEventFieldDeclaration((EventFieldDeclarationSyntax)node);
                break;
            case SyntaxKind.FieldDeclaration:
                VisitFieldDeclaration((FieldDeclarationSyntax)node);
                break;
#if ROSLYN_4_0
            case SyntaxKind.FileScopedNamespaceDeclaration:
                VisitFileScopedNamespaceDeclaration((FileScopedNamespaceDeclarationSyntax)node);
                break;
#endif
            case SyntaxKind.GlobalStatement:
                VisitGlobalStatement((GlobalStatementSyntax)node);
                break;
            case SyntaxKind.IncompleteMember:
                VisitIncompleteMember((IncompleteMemberSyntax)node);
                break;
            case SyntaxKind.IndexerDeclaration:
                VisitIndexerDeclaration((IndexerDeclarationSyntax)node);
                break;
            case SyntaxKind.InterfaceDeclaration:
                VisitInterfaceDeclaration((InterfaceDeclarationSyntax)node);
                break;
            case SyntaxKind.MethodDeclaration:
                VisitMethodDeclaration((MethodDeclarationSyntax)node);
                break;
            case SyntaxKind.NamespaceDeclaration:
                VisitNamespaceDeclaration((NamespaceDeclarationSyntax)node);
                break;
            case SyntaxKind.OperatorDeclaration:
                VisitOperatorDeclaration((OperatorDeclarationSyntax)node);
                break;
            case SyntaxKind.PropertyDeclaration:
                VisitPropertyDeclaration((PropertyDeclarationSyntax)node);
                break;
            case SyntaxKind.RecordDeclaration:
#if ROSLYN_4_0
            case SyntaxKind.RecordStructDeclaration:
#endif
                VisitRecordDeclaration((RecordDeclarationSyntax)node);
                break;
            case SyntaxKind.StructDeclaration:
                VisitStructDeclaration((StructDeclarationSyntax)node);
                break;
            default:
                Debug.Fail($"Unrecognized kind '{node.Kind()}'.");
                base.Visit(node);
                break;
        }
    }

    private void VisitAttributeLists(SyntaxList<AttributeListSyntax> attributeLists)
    {
        foreach (AttributeListSyntax attributeList in attributeLists)
        {
            if (!ShouldVisit)
                return;

            VisitAttributeList(attributeList);
        }
    }

    public static UnusedMemberWalker GetInstance()
    {
        UnusedMemberWalker walker = _cachedInstance;

        if (walker is not null)
        {
            Debug.Assert(walker._containingMethodSymbol is null);
            Debug.Assert(walker.Nodes.Count == 0);
            Debug.Assert(walker.SemanticModel is null);
            Debug.Assert(walker.CancellationToken == default);

            _cachedInstance = null;
            return walker;
        }

        return new UnusedMemberWalker();
    }

    public static void Free(UnusedMemberWalker walker)
    {
        walker.Reset();

        _cachedInstance = walker;
    }
}
